/* Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.flowable.common.engine.impl;

import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.builder.xml.XMLConfigBuilder;
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.datasource.pooled.PooledDataSource;
import org.apache.ibatis.mapping.Environment;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.defaults.DefaultSqlSessionFactory;
import org.apache.ibatis.transaction.TransactionFactory;
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
import org.apache.ibatis.transaction.managed.ManagedTransactionFactory;
import org.flowable.common.engine.api.FlowableException;
import org.flowable.common.engine.api.delegate.event.FlowableEventDispatcher;
import org.flowable.common.engine.api.delegate.event.FlowableEventListener;
import org.flowable.common.engine.impl.cfg.CommandExecutorImpl;
import org.flowable.common.engine.impl.cfg.IdGenerator;
import org.flowable.common.engine.impl.cfg.TransactionContextFactory;
import org.flowable.common.engine.impl.cfg.standalone.StandaloneMybatisTransactionContextFactory;
import org.flowable.common.engine.impl.db.*;
import org.flowable.common.engine.impl.event.EventDispatchAction;
import org.flowable.common.engine.impl.interceptor.*;
import org.flowable.common.engine.impl.persistence.GenericManagerFactory;
import org.flowable.common.engine.impl.persistence.StrongUuidGenerator;
import org.flowable.common.engine.impl.persistence.cache.EntityCache;
import org.flowable.common.engine.impl.persistence.cache.EntityCacheImpl;
import org.flowable.common.engine.impl.persistence.entity.Entity;
import org.flowable.common.engine.impl.runtime.Clock;
import org.flowable.common.engine.impl.service.CommonEngineServiceImpl;
import org.flowable.common.engine.impl.util.DefaultClockImpl;
import org.flowable.common.engine.impl.util.IoUtil;
import org.flowable.common.engine.impl.util.ReflectUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.naming.InitialContext;
import javax.sql.DataSource;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.sql.*;
import java.util.*;

public abstract class AbstractEngineConfiguration {

	protected final Logger logger = LoggerFactory.getLogger(getClass());

	/** The tenant id indicating 'no tenant' */
	public static final String NO_TENANT_ID = "";

	/**
	 * Checks the version of the DB schema against the library when the form engine is being created and throws an exception if the versions don't match.
	 */
	public static final String DB_SCHEMA_UPDATE_FALSE = "false";
	public static final String DB_SCHEMA_UPDATE_CREATE = "create";
	public static final String DB_SCHEMA_UPDATE_CREATE_DROP = "create-drop";

	/**
	 * Creates the schema when the form engine is being created and drops the schema when the form engine is being closed.
	 */
	public static final String DB_SCHEMA_UPDATE_DROP_CREATE = "drop-create";

	/**
	 * Upon building of the process engine, a check is performed and an update of the schema is performed if it is necessary.
	 */
	public static final String DB_SCHEMA_UPDATE_TRUE = "true";

	protected boolean forceCloseMybatisConnectionPool = true;

	protected String databaseType;
	protected String jdbcDriver = "org.h2.Driver";
	protected String jdbcUrl = "jdbc:h2:tcp://localhost/~/flowable";
	protected String jdbcUsername = "sa";
	protected String jdbcPassword = "";
	protected String dataSourceJndiName;
	protected int jdbcMaxActiveConnections;
	protected int jdbcMaxIdleConnections;
	protected int jdbcMaxCheckoutTime;
	protected int jdbcMaxWaitTime;
	protected boolean jdbcPingEnabled;
	protected String jdbcPingQuery;
	protected int jdbcPingConnectionNotUsedFor;
	protected int jdbcDefaultTransactionIsolationLevel;
	protected DataSource dataSource;
	protected SchemaManager commonSchemaManager;
	protected SchemaManager schemaManager;
	protected Command<Void> schemaManagementCmd;

	protected String databaseSchemaUpdate = DB_SCHEMA_UPDATE_FALSE;

	protected String xmlEncoding = "UTF-8";

	// COMMAND EXECUTORS ///////////////////////////////////////////////

	protected CommandExecutor commandExecutor;
	protected Collection<? extends CommandInterceptor> defaultCommandInterceptors;
	protected CommandConfig defaultCommandConfig;
	protected CommandConfig schemaCommandConfig;
	protected CommandContextFactory commandContextFactory;
	protected CommandInterceptor commandInvoker;

	protected List<CommandInterceptor> customPreCommandInterceptors;
	protected List<CommandInterceptor> customPostCommandInterceptors;
	protected List<CommandInterceptor> commandInterceptors;

	protected Map<String, AbstractEngineConfiguration> engineConfigurations = new HashMap<>();
	protected Map<String, AbstractServiceConfiguration> serviceConfigurations = new HashMap<>();

	protected ClassLoader classLoader;
	/**
	 * Either use Class.forName or ClassLoader.loadClass for class loading. See http://forums.activiti.org/content/reflectutilloadclass-and-custom- classloader
	 */
	protected boolean useClassForNameClassLoading = true;

	// MYBATIS SQL SESSION FACTORY /////////////////////////////////////

	protected boolean isDbHistoryUsed = true;
	protected DbSqlSessionFactory dbSqlSessionFactory;
	protected SqlSessionFactory sqlSessionFactory;
	protected TransactionFactory transactionFactory;
	protected TransactionContextFactory transactionContextFactory;

	/**
	 * If set to true, enables bulk insert (grouping sql inserts together). Default true.
	 * For some databases (eg DB2+z/OS) needs to be set to false.
	 */
	protected boolean isBulkInsertEnabled = true;

	/**
	 * Some databases have a limit of how many parameters one sql insert can have (eg SQL Server, 2000 params (!= insert statements) ). Tweak this parameter in case of exceptions indicating too much
	 * is being put into one bulk insert, or make it higher if your database can cope with it and there are inserts with a huge amount of data.
	 * <p>
	 * By default: 100 (75 for mssql server as it has a hard limit of 2000 parameters in a statement)
	 */
	protected int maxNrOfStatementsInBulkInsert = 100;

	public int DEFAULT_MAX_NR_OF_STATEMENTS_BULK_INSERT_SQL_SERVER = 60; // currently Execution has most params (31). 2000 / 31 = 64.

	protected Set<Class<?>> customMybatisMappers;
	protected Set<String> customMybatisXMLMappers;
	protected List<Interceptor> customMybatisInterceptors;


	protected Set<String> dependentEngineMyBatisXmlMappers;
	protected List<MybatisTypeAliasConfigurator> dependentEngineMybatisTypeAliasConfigs;
	protected List<MybatisTypeHandlerConfigurator> dependentEngineMybatisTypeHandlerConfigs;

	// SESSION FACTORIES ///////////////////////////////////////////////
	protected List<SessionFactory> customSessionFactories;
	protected Map<Class<?>, SessionFactory> sessionFactories;

	protected boolean enableEventDispatcher = true;
	protected FlowableEventDispatcher eventDispatcher;
	protected List<FlowableEventListener> eventListeners;
	protected Map<String, List<FlowableEventListener>> typedEventListeners;
	protected List<EventDispatchAction> additionalEventDispatchActions;

	protected boolean transactionsExternallyManaged;

	/**
	 * Flag that can be set to configure or not a relational database is used. This is useful for custom implementations that do not use relational databases at all.
	 *
	 * If true (default), the {@link AbstractEngineConfiguration#getDatabaseSchemaUpdate()} value will be used to determine what needs to happen wrt the database schema.
	 *
	 * If false, no validation or schema creation will be done. That means that the database schema must have been created 'manually' before but the engine does not validate whether the schema is
	 * correct. The {@link AbstractEngineConfiguration#getDatabaseSchemaUpdate()} value will not be used.
	 */
	protected boolean usingRelationalDatabase = true;

	/**
	 * Flag that can be set to configure whether or not a schema is used. This is usefil for custom implementations that do not use relational databases at all.
	 * Setting {@link #usingRelationalDatabase} to true will automotically imply using a schema.
	 */
	protected boolean usingSchemaMgmt = true;

	/**
	 * Allows configuring a database table prefix which is used for all runtime operations of the process engine. For example, if you specify a prefix named 'PRE1.', Flowable will query for executions
	 * in a table named 'PRE1.ACT_RU_EXECUTION_'.
	 *
	 * <p />
	 * <strong>NOTE: the prefix is not respected by automatic database schema management. If you use {@link AbstractEngineConfiguration#DB_SCHEMA_UPDATE_CREATE_DROP} or
	 * {@link AbstractEngineConfiguration#DB_SCHEMA_UPDATE_TRUE}, Flowable will create the database tables using the default names, regardless of the prefix configured here.</strong>
	 */
	protected String databaseTablePrefix = "";

	/**
	 * Escape character for doing wildcard searches.
	 *
	 * This will be added at then end of queries that include for example a LIKE clause. For example: SELECT * FROM table WHERE column LIKE '%\%%' ESCAPE '\';
	 */
	protected String databaseWildcardEscapeCharacter;

	/**
	 * database catalog to use
	 */
	protected String databaseCatalog = "";

	/**
	 * In some situations you want to set the schema to use for table checks / generation if the database metadata doesn't return that correctly, see https://jira.codehaus.org/browse/ACT-1220,
	 * https://jira.codehaus.org/browse/ACT-1062
	 */
	protected String databaseSchema;

	/**
	 * Set to true in case the defined databaseTablePrefix is a schema-name, instead of an actual table name prefix. This is relevant for checking if Flowable-tables exist, the databaseTablePrefix
	 * will not be used here - since the schema is taken into account already, adding a prefix for the table-check will result in wrong table-names.
	 */
	protected boolean tablePrefixIsSchema;

	/**
	 * Set to true if the latest version of a definition should be retrieved, ignoring a possible parent deployment id value
	 */
	protected boolean alwaysLookupLatestDefinitionVersion;

	/**
	 * Set to true if by default lookups should fallback to the default tenant (an empty string by default or a defined tenant value)
	 */
	protected boolean fallbackToDefaultTenant;

	/**
	 * Default tenant provider that is executed when looking up definitions, in case the global or local fallback to default tenant value is true
	 */
	protected DefaultTenantProvider defaultTenantProvider = (tenantId, scope, scopeKey) -> NO_TENANT_ID;

	/**
	 * Enables the MyBatis plugin that logs the execution time of sql statements.
	 */
	protected boolean enableLogSqlExecutionTime;

	protected Properties databaseTypeMappings = getDefaultDatabaseTypeMappings();

	protected List<EngineDeployer> customPreDeployers;
	protected List<EngineDeployer> customPostDeployers;
	protected List<EngineDeployer> deployers;

	// CONFIGURATORS ////////////////////////////////////////////////////////////

	protected boolean enableConfiguratorServiceLoader = true; // Enabled by default. In certain environments this should be set to false (eg osgi)
	protected List<EngineConfigurator> configurators; // The injected configurators
	protected List<EngineConfigurator> allConfigurators; // Including auto-discovered configurators
	protected EngineConfigurator idmEngineConfigurator;

	public static final String PRODUCT_NAME_POSTGRES = "PostgreSQL";
	public static final String PRODUCT_NAME_CRDB = "CockroachDB";

	public static final String DATABASE_TYPE_H2 = "h2";
	public static final String DATABASE_TYPE_HSQL = "hsql";
	public static final String DATABASE_TYPE_MYSQL = "mysql";
	public static final String DATABASE_TYPE_ORACLE = "oracle";
	public static final String DATABASE_TYPE_POSTGRES = "postgres";
	public static final String DATABASE_TYPE_MSSQL = "mssql";
	public static final String DATABASE_TYPE_DB2 = "db2";
	public static final String DATABASE_TYPE_COCKROACHDB = "cockroachdb";
	public static final String DATABASE_TYPE_DM = "oracle";

	public static Properties getDefaultDatabaseTypeMappings() {
		Properties databaseTypeMappings = new Properties();
		databaseTypeMappings.setProperty("H2", DATABASE_TYPE_H2);
		databaseTypeMappings.setProperty("HSQL Database Engine", DATABASE_TYPE_HSQL);
		databaseTypeMappings.setProperty("MySQL", DATABASE_TYPE_MYSQL);
		databaseTypeMappings.setProperty("MariaDB", DATABASE_TYPE_MYSQL);
		databaseTypeMappings.setProperty("Oracle", DATABASE_TYPE_ORACLE);
		databaseTypeMappings.setProperty(PRODUCT_NAME_POSTGRES, DATABASE_TYPE_POSTGRES);
		databaseTypeMappings.setProperty("Microsoft SQL Server", DATABASE_TYPE_MSSQL);
		databaseTypeMappings.setProperty(DATABASE_TYPE_DB2, DATABASE_TYPE_DB2);
		databaseTypeMappings.setProperty("DB2", DATABASE_TYPE_DB2);
		databaseTypeMappings.setProperty("DB2/NT", DATABASE_TYPE_DB2);
		databaseTypeMappings.setProperty("DB2/NT64", DATABASE_TYPE_DB2);
		databaseTypeMappings.setProperty("DB2 UDP", DATABASE_TYPE_DB2);
		databaseTypeMappings.setProperty("DB2/LINUX", DATABASE_TYPE_DB2);
		databaseTypeMappings.setProperty("DB2/LINUX390", DATABASE_TYPE_DB2);
		databaseTypeMappings.setProperty("DB2/LINUXX8664", DATABASE_TYPE_DB2);
		databaseTypeMappings.setProperty("DB2/LINUXZ64", DATABASE_TYPE_DB2);
		databaseTypeMappings.setProperty("DB2/LINUXPPC64", DATABASE_TYPE_DB2);
		databaseTypeMappings.setProperty("DB2/400 SQL", DATABASE_TYPE_DB2);
		databaseTypeMappings.setProperty("DB2/6000", DATABASE_TYPE_DB2);
		databaseTypeMappings.setProperty("DB2 UDB iSeries", DATABASE_TYPE_DB2);
		databaseTypeMappings.setProperty("DB2/AIX64", DATABASE_TYPE_DB2);
		databaseTypeMappings.setProperty("DB2/HPUX", DATABASE_TYPE_DB2);
		databaseTypeMappings.setProperty("DB2/HP64", DATABASE_TYPE_DB2);
		databaseTypeMappings.setProperty("DB2/SUN", DATABASE_TYPE_DB2);
		databaseTypeMappings.setProperty("DB2/SUN64", DATABASE_TYPE_DB2);
		databaseTypeMappings.setProperty("DB2/PTX", DATABASE_TYPE_DB2);
		databaseTypeMappings.setProperty("DB2/2", DATABASE_TYPE_DB2);
		databaseTypeMappings.setProperty("DB2 UDB AS400", DATABASE_TYPE_DB2);
		databaseTypeMappings.setProperty("DM DBMS", DATABASE_TYPE_DM);
		databaseTypeMappings.setProperty(PRODUCT_NAME_CRDB, DATABASE_TYPE_COCKROACHDB);
		return databaseTypeMappings;
	}

	protected Map<Object, Object> beans;

	protected IdGenerator idGenerator;
	protected boolean usePrefixId;

	protected Clock clock;

	// Variables

	public static final int DEFAULT_GENERIC_MAX_LENGTH_STRING = 4000;
	public static final int DEFAULT_ORACLE_MAX_LENGTH_STRING = 2000;

	/**
	 * Define a max length for storing String variable types in the database. Mainly used for the Oracle NVARCHAR2 limit of 2000 characters
	 */
	protected int maxLengthStringVariableType = -1;

	protected void initEngineConfigurations() {
		engineConfigurations.put(getEngineCfgKey(), this);
	}

	// DataSource
	// ///////////////////////////////////////////////////////////////

	protected void initDataSource() {
		if (dataSource == null) {
			if (dataSourceJndiName != null) {
				try {
					dataSource = (DataSource) new InitialContext().lookup(dataSourceJndiName);
				} catch (Exception e) {
					throw new FlowableException("couldn't lookup datasource from " + dataSourceJndiName + ": " + e.getMessage(), e);
				}

			} else if (jdbcUrl != null) {
				if ((jdbcDriver == null) || (jdbcUsername == null)) {
					throw new FlowableException("DataSource or JDBC properties have to be specified in a process engine configuration");
				}

				logger.debug("initializing datasource to db: {}", jdbcUrl);

				if (logger.isInfoEnabled()) {
					logger.info("Configuring Datasource with following properties (omitted password for security)");
					logger.info("datasource driver : {}", jdbcDriver);
					logger.info("datasource url : {}", jdbcUrl);
					logger.info("datasource user name : {}", jdbcUsername);
				}

				PooledDataSource pooledDataSource = new PooledDataSource(this.getClass().getClassLoader(), jdbcDriver, jdbcUrl, jdbcUsername, jdbcPassword);

				if (jdbcMaxActiveConnections > 0) {
					pooledDataSource.setPoolMaximumActiveConnections(jdbcMaxActiveConnections);
				}
				if (jdbcMaxIdleConnections > 0) {
					pooledDataSource.setPoolMaximumIdleConnections(jdbcMaxIdleConnections);
				}
				if (jdbcMaxCheckoutTime > 0) {
					pooledDataSource.setPoolMaximumCheckoutTime(jdbcMaxCheckoutTime);
				}
				if (jdbcMaxWaitTime > 0) {
					pooledDataSource.setPoolTimeToWait(jdbcMaxWaitTime);
				}
				if (jdbcPingEnabled) {
					pooledDataSource.setPoolPingEnabled(true);
					if (jdbcPingQuery != null) {
						pooledDataSource.setPoolPingQuery(jdbcPingQuery);
					}
					pooledDataSource.setPoolPingConnectionsNotUsedFor(jdbcPingConnectionNotUsedFor);
				}
				if (jdbcDefaultTransactionIsolationLevel > 0) {
					pooledDataSource.setDefaultTransactionIsolationLevel(jdbcDefaultTransactionIsolationLevel);
				}
				dataSource = pooledDataSource;
			}
		}

		if (databaseType == null) {
			initDatabaseType();
		}
	}

	public void initDatabaseType() {
		Connection connection = null;
		try {
			connection = dataSource.getConnection();
			DatabaseMetaData databaseMetaData = connection.getMetaData();
			String databaseProductName = databaseMetaData.getDatabaseProductName();
			logger.debug("database product name: '{}'", databaseProductName);

			// CRDB does not expose the version through the jdbc driver, so we need to fetch it through version().
			if (PRODUCT_NAME_POSTGRES.equalsIgnoreCase(databaseProductName)) {
				PreparedStatement preparedStatement = connection.prepareStatement("select version() as version;");
				ResultSet resultSet = preparedStatement.executeQuery();
				String version = null;
				if (resultSet.next()) {
					version = resultSet.getString("version");
				}
				resultSet.close();

				if (StringUtils.isNotEmpty(version) && version.toLowerCase().startsWith(PRODUCT_NAME_CRDB.toLowerCase())) {
					databaseProductName = PRODUCT_NAME_CRDB;
					logger.info("CockroachDB version '{}' detected", version);
				}
			}

			databaseType = databaseTypeMappings.getProperty(databaseProductName);
			if (databaseType == null) {
				throw new FlowableException("couldn't deduct database type from database product name '" + databaseProductName + "'");
			}
			logger.debug("using database type: {}", databaseType);

		} catch (SQLException e) {
			logger.error("Exception while initializing Database connection", e);
		} finally {
			try {
				if (connection != null) {
					connection.close();
				}
			} catch (SQLException e) {
				logger.error("Exception while closing the Database connection", e);
			}
		}

		// Special care for MSSQL, as it has a hard limit of 2000 params per statement (incl bulk statement).
		// Especially with executions, with 100 as default, this limit is passed.
		if (DATABASE_TYPE_MSSQL.equals(databaseType)) {
			maxNrOfStatementsInBulkInsert = DEFAULT_MAX_NR_OF_STATEMENTS_BULK_INSERT_SQL_SERVER;
		}
	}

	public void initSchemaManager() {
		if (this.commonSchemaManager == null) {
			this.commonSchemaManager = new CommonDbSchemaManager();
		}
	}

	// session factories ////////////////////////////////////////////////////////

	public void addSessionFactory(SessionFactory sessionFactory) {
		sessionFactories.put(sessionFactory.getSessionType(), sessionFactory);
	}

	public void initCommandContextFactory() {
		if (commandContextFactory == null) {
			commandContextFactory = new CommandContextFactory();
		}
	}

	public void initTransactionContextFactory() {
		if (transactionContextFactory == null) {
			transactionContextFactory = new StandaloneMybatisTransactionContextFactory();
		}
	}

	public void initCommandExecutors() {
		initDefaultCommandConfig();
		initSchemaCommandConfig();
		initCommandInvoker();
		initCommandInterceptors();
		initCommandExecutor();
	}


	public void initDefaultCommandConfig() {
		if (defaultCommandConfig == null) {
			defaultCommandConfig = new CommandConfig();
		}
	}

	public void initSchemaCommandConfig() {
		if (schemaCommandConfig == null) {
			schemaCommandConfig = new CommandConfig();
		}
	}

	public void initCommandInvoker() {
		if (commandInvoker == null) {
			commandInvoker = new DefaultCommandInvoker();
		}
	}

	public void initCommandInterceptors() {
		if (commandInterceptors == null) {
			commandInterceptors = new ArrayList<>();
			if (customPreCommandInterceptors != null) {
				commandInterceptors.addAll(customPreCommandInterceptors);
			}
			commandInterceptors.addAll(getDefaultCommandInterceptors());
			if (customPostCommandInterceptors != null) {
				commandInterceptors.addAll(customPostCommandInterceptors);
			}
			commandInterceptors.add(commandInvoker);
		}
	}

	public Collection<? extends CommandInterceptor> getDefaultCommandInterceptors() {
		if (defaultCommandInterceptors == null) {
			List<CommandInterceptor> interceptors = new ArrayList<>();
			interceptors.add(new LogInterceptor());

			if (DATABASE_TYPE_COCKROACHDB.equals(databaseType)) {
				interceptors.add(new CrDbRetryInterceptor());
			}

			CommandInterceptor transactionInterceptor = createTransactionInterceptor();
			if (transactionInterceptor != null) {
				interceptors.add(transactionInterceptor);
			}

			if (commandContextFactory != null) {
				String engineCfgKey = getEngineCfgKey();
				CommandContextInterceptor commandContextInterceptor = new CommandContextInterceptor(commandContextFactory);
				engineConfigurations.put(engineCfgKey, this);
				commandContextInterceptor.setEngineConfigurations(engineConfigurations);
				commandContextInterceptor.setServiceConfigurations(serviceConfigurations);
				commandContextInterceptor.setCurrentEngineConfigurationKey(engineCfgKey);
				interceptors.add(commandContextInterceptor);
			}

			if (transactionContextFactory != null) {
				interceptors.add(new TransactionContextInterceptor(transactionContextFactory));
			}

			List<CommandInterceptor> additionalCommandInterceptors = getAdditionalDefaultCommandInterceptors();
			if (additionalCommandInterceptors != null) {
				interceptors.addAll(additionalCommandInterceptors);
			}

			defaultCommandInterceptors = interceptors;
		}
		return defaultCommandInterceptors;
	}

	public abstract String getEngineCfgKey();

	public List<CommandInterceptor> getAdditionalDefaultCommandInterceptors() {
		return null;
	}

	public void initCommandExecutor() {
		if (commandExecutor == null) {
			CommandInterceptor first = initInterceptorChain(commandInterceptors);
			commandExecutor = new CommandExecutorImpl(getDefaultCommandConfig(), first);
		}
	}

	public CommandInterceptor initInterceptorChain(List<CommandInterceptor> chain) {
		if (chain == null || chain.isEmpty()) {
			throw new FlowableException("invalid command interceptor chain configuration: " + chain);
		}
		for (int i = 0; i < chain.size() - 1; i++) {
			chain.get(i).setNext(chain.get(i + 1));
		}
		return chain.get(0);
	}

	public abstract CommandInterceptor createTransactionInterceptor();


	public void initBeans() {
		if (beans == null) {
			beans = new HashMap<>();
		}
	}

	// id generator
	// /////////////////////////////////////////////////////////////

	public void initIdGenerator() {
		if (idGenerator == null) {
			idGenerator = new StrongUuidGenerator();
		}
	}

	public void initClock() {
		if (clock == null) {
			clock = new DefaultClockImpl();
		}
	}

	// services
	// /////////////////////////////////////////////////////////////////

	protected void initService(Object service) {
		if (service instanceof CommonEngineServiceImpl) {
			((CommonEngineServiceImpl) service).setCommandExecutor(commandExecutor);
		}
	}

	// myBatis SqlSessionFactory
	// ////////////////////////////////////////////////

	public void initSessionFactories() {
		if (sessionFactories == null) {
			sessionFactories = new HashMap<>();

			if (usingRelationalDatabase) {
				initDbSqlSessionFactory();
			}

			addSessionFactory(new GenericManagerFactory(EntityCache.class, EntityCacheImpl.class));
			commandContextFactory.setSessionFactories(sessionFactories);
		}

		if (customSessionFactories != null) {
			for (SessionFactory sessionFactory : customSessionFactories) {
				addSessionFactory(sessionFactory);
			}
		}
	}

	public void initDbSqlSessionFactory() {
		if (dbSqlSessionFactory == null) {
			dbSqlSessionFactory = createDbSqlSessionFactory();
		}
		dbSqlSessionFactory.setDatabaseType(databaseType);
		dbSqlSessionFactory.setSqlSessionFactory(sqlSessionFactory);
		dbSqlSessionFactory.setDbHistoryUsed(isDbHistoryUsed);
		dbSqlSessionFactory.setDatabaseTablePrefix(databaseTablePrefix);
		dbSqlSessionFactory.setTablePrefixIsSchema(tablePrefixIsSchema);
		dbSqlSessionFactory.setDatabaseCatalog(databaseCatalog);
		dbSqlSessionFactory.setDatabaseSchema(databaseSchema);
		dbSqlSessionFactory.setMaxNrOfStatementsInBulkInsert(maxNrOfStatementsInBulkInsert);

		initDbSqlSessionFactoryEntitySettings();

		addSessionFactory(dbSqlSessionFactory);
	}

	public DbSqlSessionFactory createDbSqlSessionFactory() {
		return new DbSqlSessionFactory(usePrefixId);
	}

	protected abstract void initDbSqlSessionFactoryEntitySettings();

	protected void defaultInitDbSqlSessionFactoryEntitySettings(List<Class<? extends Entity>> insertOrder, List<Class<? extends Entity>> deleteOrder) {
		if (insertOrder != null) {
			for (Class<? extends Entity> clazz : insertOrder) {
				dbSqlSessionFactory.getInsertionOrder().add(clazz);

				if (isBulkInsertEnabled) {
					dbSqlSessionFactory.getBulkInserteableEntityClasses().add(clazz);
				}
			}
		}

		if (deleteOrder != null) {
			for (Class<? extends Entity> clazz : deleteOrder) {
				dbSqlSessionFactory.getDeletionOrder().add(clazz);
			}
		}
	}

	public void initTransactionFactory() {
		if (transactionFactory == null) {
			if (transactionsExternallyManaged) {
				transactionFactory = new ManagedTransactionFactory();
				Properties properties = new Properties();
				properties.put("closeConnection", "false");
				this.transactionFactory.setProperties(properties);
			} else {
				transactionFactory = new JdbcTransactionFactory();
			}
		}
	}

	public void initSqlSessionFactory() {
		if (sqlSessionFactory == null) {
			InputStream inputStream = null;
			try {
				inputStream = getMyBatisXmlConfigurationStream();

				Environment environment = new Environment("default", transactionFactory, dataSource);
				Reader reader = new InputStreamReader(inputStream);
				Properties properties = new Properties();
				properties.put("prefix", databaseTablePrefix);

				String wildcardEscapeClause = "";
				if ((databaseWildcardEscapeCharacter != null) && (databaseWildcardEscapeCharacter.length() != 0)) {
					wildcardEscapeClause = " escape '" + databaseWildcardEscapeCharacter + "'";
				}
				properties.put("wildcardEscapeClause", wildcardEscapeClause);

				// set default properties
				properties.put("limitBefore", "");
				properties.put("limitAfter", "");
				properties.put("limitBetween", "");
				properties.put("limitOuterJoinBetween", "");
				properties.put("limitBeforeNativeQuery", "");
				properties.put("blobType", "BLOB");
				properties.put("boolValue", "TRUE");

				if (databaseType != null) {
					properties.load(getResourceAsStream(pathToEngineDbProperties()));
				}

				Configuration configuration = initMybatisConfiguration(environment, reader, properties);
				sqlSessionFactory = new DefaultSqlSessionFactory(configuration);

			} catch (Exception e) {
				throw new FlowableException("Error while building ibatis SqlSessionFactory: " + e.getMessage(), e);
			} finally {
				IoUtil.closeSilently(inputStream);
			}
		}
	}

	public String pathToEngineDbProperties() {
		return "org/flowable/common/db/properties/" + databaseType + ".properties";
	}

	public Configuration initMybatisConfiguration(Environment environment, Reader reader, Properties properties) {
		XMLConfigBuilder parser = new XMLConfigBuilder(reader, "", properties);
		Configuration configuration = parser.getConfiguration();

		if (databaseType != null) {
			configuration.setDatabaseId(databaseType);
		}

		configuration.setEnvironment(environment);

		initCustomMybatisMappers(configuration);
		initMybatisTypeHandlers(configuration);
		initCustomMybatisInterceptors(configuration);
		if (isEnableLogSqlExecutionTime()) {
			initMyBatisLogSqlExecutionTimePlugin(configuration);
		}

		configuration = parseMybatisConfiguration(parser);
		return configuration;
	}

	public void initCustomMybatisMappers(Configuration configuration) {
		if (getCustomMybatisMappers() != null) {
			for (Class<?> clazz : getCustomMybatisMappers()) {
				configuration.addMapper(clazz);
			}
		}
	}

	public void initMybatisTypeHandlers(Configuration configuration) {
		// To be extended
	}

	public void initCustomMybatisInterceptors(Configuration configuration) {
		if (customMybatisInterceptors!=null){
			for (Interceptor interceptor :customMybatisInterceptors){
				configuration.addInterceptor(interceptor);
			}
		}
	}

	public void initMyBatisLogSqlExecutionTimePlugin(Configuration configuration) {
		configuration.addInterceptor(new LogSqlExecutionTimePlugin());
	}

	public Configuration parseMybatisConfiguration(XMLConfigBuilder parser) {
		Configuration configuration = parser.parse();

		if (dependentEngineMybatisTypeAliasConfigs != null) {
			for (MybatisTypeAliasConfigurator typeAliasConfig : dependentEngineMybatisTypeAliasConfigs) {
				typeAliasConfig.configure(configuration.getTypeAliasRegistry());
			}
		}
		if (dependentEngineMybatisTypeHandlerConfigs != null) {
			for (MybatisTypeHandlerConfigurator typeHandlerConfig : dependentEngineMybatisTypeHandlerConfigs) {
				typeHandlerConfig.configure(configuration.getTypeHandlerRegistry());
			}
		}

		parseDependentEngineMybatisXMLMappers(configuration);
		parseCustomMybatisXMLMappers(configuration);
		return configuration;
	}

	public void parseCustomMybatisXMLMappers(Configuration configuration) {
		if (getCustomMybatisXMLMappers() != null) {
			for (String resource : getCustomMybatisXMLMappers()) {
				parseMybatisXmlMapping(configuration, resource);
			}
		}
	}

	public void parseDependentEngineMybatisXMLMappers(Configuration configuration) {
		if (getDependentEngineMyBatisXmlMappers() != null) {
			for (String resource : getDependentEngineMyBatisXmlMappers()) {
				parseMybatisXmlMapping(configuration, resource);
			}
		}
	}

	protected void parseMybatisXmlMapping(Configuration configuration, String resource) {
		// see XMLConfigBuilder.mapperElement()
		XMLMapperBuilder mapperParser = new XMLMapperBuilder(getResourceAsStream(resource), configuration, resource, configuration.getSqlFragments());
		mapperParser.parse();
	}

	protected InputStream getResourceAsStream(String resource) {
		ClassLoader classLoader = getClassLoader();
		if (classLoader != null) {
			return getClassLoader().getResourceAsStream(resource);
		} else {
			return this.getClass().getClassLoader().getResourceAsStream(resource);
		}
	}

	public abstract InputStream getMyBatisXmlConfigurationStream();

	public void initConfigurators() {

		allConfigurators = new ArrayList<>();
		allConfigurators.addAll(getEngineSpecificEngineConfigurators());

		// Configurators that are explicitly added to the config
		if (configurators != null) {
			allConfigurators.addAll(configurators);
		}

		// Auto discovery through ServiceLoader
		if (enableConfiguratorServiceLoader) {
			ClassLoader classLoader = getClassLoader();
			if (classLoader == null) {
				classLoader = ReflectUtil.getClassLoader();
			}

			ServiceLoader<EngineConfigurator> configuratorServiceLoader = ServiceLoader.load(EngineConfigurator.class, classLoader);
			int nrOfServiceLoadedConfigurators = 0;
			for (EngineConfigurator configurator : configuratorServiceLoader) {
				allConfigurators.add(configurator);
				nrOfServiceLoadedConfigurators++;
			}

			if (nrOfServiceLoadedConfigurators > 0) {
				logger.info("Found {} auto-discoverable Process Engine Configurator{}", nrOfServiceLoadedConfigurators, nrOfServiceLoadedConfigurators > 1 ? "s" : "");
			}

			if (!allConfigurators.isEmpty()) {

				// Order them according to the priorities (useful for dependent
				// configurator)
				Collections.sort(allConfigurators, new Comparator<EngineConfigurator>() {
					@Override
					public int compare(EngineConfigurator configurator1, EngineConfigurator configurator2) {
						int priority1 = configurator1.getPriority();
						int priority2 = configurator2.getPriority();

						if (priority1 < priority2) {
							return -1;
						} else if (priority1 > priority2) {
							return 1;
						}
						return 0;
					}
				});

				// Execute the configurators
				logger.info("Found {} Engine Configurators in total:", allConfigurators.size());
				for (EngineConfigurator configurator : allConfigurators) {
					logger.info("{} (priority:{})", configurator.getClass(), configurator.getPriority());
				}

			}

		}
	}

	public void close() {
		if (forceCloseMybatisConnectionPool && dataSource instanceof PooledDataSource) {
			/*
			 * When the datasource is created by a Flowable engine (i.e. it's an instance of PooledDataSource),
			 * the connection pool needs to be closed when closing the engine.
			 * Note that calling forceCloseAll() multiple times (as is the case when running with multiple engine) is ok.
			 */
			((PooledDataSource) dataSource).forceCloseAll();
		}
	}

	protected List<EngineConfigurator> getEngineSpecificEngineConfigurators() {
		// meant to be overridden if needed
		return Collections.emptyList();
	}

	public void configuratorsBeforeInit() {
		for (EngineConfigurator configurator : allConfigurators) {
			logger.info("Executing beforeInit() of {} (priority:{})", configurator.getClass(), configurator.getPriority());
			configurator.beforeInit(this);
		}
	}

	public void configuratorsAfterInit() {
		for (EngineConfigurator configurator : allConfigurators) {
			logger.info("Executing configure() of {} (priority:{})", configurator.getClass(), configurator.getPriority());
			configurator.configure(this);
		}
	}

	// getters and setters
	// //////////////////////////////////////////////////////

	public abstract String getEngineName();

	public ClassLoader getClassLoader() {
		return classLoader;
	}

	public AbstractEngineConfiguration setClassLoader(ClassLoader classLoader) {
		this.classLoader = classLoader;
		return this;
	}

	public boolean isUseClassForNameClassLoading() {
		return useClassForNameClassLoading;
	}

	public AbstractEngineConfiguration setUseClassForNameClassLoading(boolean useClassForNameClassLoading) {
		this.useClassForNameClassLoading = useClassForNameClassLoading;
		return this;
	}

	public String getDatabaseType() {
		return databaseType;
	}

	public AbstractEngineConfiguration setDatabaseType(String databaseType) {
		this.databaseType = databaseType;
		return this;
	}

	public DataSource getDataSource() {
		return dataSource;
	}

	public AbstractEngineConfiguration setDataSource(DataSource dataSource) {
		this.dataSource = dataSource;
		return this;
	}

	public SchemaManager getSchemaManager() {
		return schemaManager;
	}

	public AbstractEngineConfiguration setSchemaManager(SchemaManager schemaManager) {
		this.schemaManager = schemaManager;
		return this;
	}

	public SchemaManager getCommonSchemaManager() {
		return commonSchemaManager;
	}

	public AbstractEngineConfiguration setCommonSchemaManager(SchemaManager commonSchemaManager) {
		this.commonSchemaManager = commonSchemaManager;
		return this;
	}

	public Command<Void> getSchemaManagementCmd() {
		return schemaManagementCmd;
	}

	public AbstractEngineConfiguration setSchemaManagementCmd(Command<Void> schemaManagementCmd) {
		this.schemaManagementCmd = schemaManagementCmd;
		return this;
	}

	public String getJdbcDriver() {
		return jdbcDriver;
	}

	public AbstractEngineConfiguration setJdbcDriver(String jdbcDriver) {
		this.jdbcDriver = jdbcDriver;
		return this;
	}

	public String getJdbcUrl() {
		return jdbcUrl;
	}

	public AbstractEngineConfiguration setJdbcUrl(String jdbcUrl) {
		this.jdbcUrl = jdbcUrl;
		return this;
	}

	public String getJdbcUsername() {
		return jdbcUsername;
	}

	public AbstractEngineConfiguration setJdbcUsername(String jdbcUsername) {
		this.jdbcUsername = jdbcUsername;
		return this;
	}

	public String getJdbcPassword() {
		return jdbcPassword;
	}

	public AbstractEngineConfiguration setJdbcPassword(String jdbcPassword) {
		this.jdbcPassword = jdbcPassword;
		return this;
	}

	public int getJdbcMaxActiveConnections() {
		return jdbcMaxActiveConnections;
	}

	public AbstractEngineConfiguration setJdbcMaxActiveConnections(int jdbcMaxActiveConnections) {
		this.jdbcMaxActiveConnections = jdbcMaxActiveConnections;
		return this;
	}

	public int getJdbcMaxIdleConnections() {
		return jdbcMaxIdleConnections;
	}

	public AbstractEngineConfiguration setJdbcMaxIdleConnections(int jdbcMaxIdleConnections) {
		this.jdbcMaxIdleConnections = jdbcMaxIdleConnections;
		return this;
	}

	public int getJdbcMaxCheckoutTime() {
		return jdbcMaxCheckoutTime;
	}

	public AbstractEngineConfiguration setJdbcMaxCheckoutTime(int jdbcMaxCheckoutTime) {
		this.jdbcMaxCheckoutTime = jdbcMaxCheckoutTime;
		return this;
	}

	public int getJdbcMaxWaitTime() {
		return jdbcMaxWaitTime;
	}

	public AbstractEngineConfiguration setJdbcMaxWaitTime(int jdbcMaxWaitTime) {
		this.jdbcMaxWaitTime = jdbcMaxWaitTime;
		return this;
	}

	public boolean isJdbcPingEnabled() {
		return jdbcPingEnabled;
	}

	public AbstractEngineConfiguration setJdbcPingEnabled(boolean jdbcPingEnabled) {
		this.jdbcPingEnabled = jdbcPingEnabled;
		return this;
	}

	public int getJdbcPingConnectionNotUsedFor() {
		return jdbcPingConnectionNotUsedFor;
	}

	public AbstractEngineConfiguration setJdbcPingConnectionNotUsedFor(int jdbcPingConnectionNotUsedFor) {
		this.jdbcPingConnectionNotUsedFor = jdbcPingConnectionNotUsedFor;
		return this;
	}

	public int getJdbcDefaultTransactionIsolationLevel() {
		return jdbcDefaultTransactionIsolationLevel;
	}

	public AbstractEngineConfiguration setJdbcDefaultTransactionIsolationLevel(int jdbcDefaultTransactionIsolationLevel) {
		this.jdbcDefaultTransactionIsolationLevel = jdbcDefaultTransactionIsolationLevel;
		return this;
	}

	public String getJdbcPingQuery() {
		return jdbcPingQuery;
	}

	public AbstractEngineConfiguration setJdbcPingQuery(String jdbcPingQuery) {
		this.jdbcPingQuery = jdbcPingQuery;
		return this;
	}

	public String getDataSourceJndiName() {
		return dataSourceJndiName;
	}

	public AbstractEngineConfiguration setDataSourceJndiName(String dataSourceJndiName) {
		this.dataSourceJndiName = dataSourceJndiName;
		return this;
	}

	public CommandConfig getSchemaCommandConfig() {
		return schemaCommandConfig;
	}

	public AbstractEngineConfiguration setSchemaCommandConfig(CommandConfig schemaCommandConfig) {
		this.schemaCommandConfig = schemaCommandConfig;
		return this;
	}

	public boolean isTransactionsExternallyManaged() {
		return transactionsExternallyManaged;
	}

	public AbstractEngineConfiguration setTransactionsExternallyManaged(boolean transactionsExternallyManaged) {
		this.transactionsExternallyManaged = transactionsExternallyManaged;
		return this;
	}

	public Map<Object, Object> getBeans() {
		return beans;
	}

	public AbstractEngineConfiguration setBeans(Map<Object, Object> beans) {
		this.beans = beans;
		return this;
	}

	public IdGenerator getIdGenerator() {
		return idGenerator;
	}

	public AbstractEngineConfiguration setIdGenerator(IdGenerator idGenerator) {
		this.idGenerator = idGenerator;
		return this;
	}

	public boolean isUsePrefixId() {
		return usePrefixId;
	}

	public AbstractEngineConfiguration setUsePrefixId(boolean usePrefixId) {
		this.usePrefixId = usePrefixId;
		return this;
	}

	public String getXmlEncoding() {
		return xmlEncoding;
	}

	public AbstractEngineConfiguration setXmlEncoding(String xmlEncoding) {
		this.xmlEncoding = xmlEncoding;
		return this;
	}

	public CommandConfig getDefaultCommandConfig() {
		return defaultCommandConfig;
	}

	public AbstractEngineConfiguration setDefaultCommandConfig(CommandConfig defaultCommandConfig) {
		this.defaultCommandConfig = defaultCommandConfig;
		return this;
	}

	public CommandExecutor getCommandExecutor() {
		return commandExecutor;
	}

	public AbstractEngineConfiguration setCommandExecutor(CommandExecutor commandExecutor) {
		this.commandExecutor = commandExecutor;
		return this;
	}

	public CommandContextFactory getCommandContextFactory() {
		return commandContextFactory;
	}

	public AbstractEngineConfiguration setCommandContextFactory(CommandContextFactory commandContextFactory) {
		this.commandContextFactory = commandContextFactory;
		return this;
	}

	public CommandInterceptor getCommandInvoker() {
		return commandInvoker;
	}

	public AbstractEngineConfiguration setCommandInvoker(CommandInterceptor commandInvoker) {
		this.commandInvoker = commandInvoker;
		return this;
	}

	public List<CommandInterceptor> getCustomPreCommandInterceptors() {
		return customPreCommandInterceptors;
	}

	public AbstractEngineConfiguration setCustomPreCommandInterceptors(List<CommandInterceptor> customPreCommandInterceptors) {
		this.customPreCommandInterceptors = customPreCommandInterceptors;
		return this;
	}

	public List<CommandInterceptor> getCustomPostCommandInterceptors() {
		return customPostCommandInterceptors;
	}

	public AbstractEngineConfiguration setCustomPostCommandInterceptors(List<CommandInterceptor> customPostCommandInterceptors) {
		this.customPostCommandInterceptors = customPostCommandInterceptors;
		return this;
	}

	public List<CommandInterceptor> getCommandInterceptors() {
		return commandInterceptors;
	}

	public AbstractEngineConfiguration setCommandInterceptors(List<CommandInterceptor> commandInterceptors) {
		this.commandInterceptors = commandInterceptors;
		return this;
	}

	public Map<String, AbstractEngineConfiguration> getEngineConfigurations() {
		return engineConfigurations;
	}

	public AbstractEngineConfiguration setEngineConfigurations(Map<String, AbstractEngineConfiguration> engineConfigurations) {
		this.engineConfigurations = engineConfigurations;
		return this;
	}

	public void addEngineConfiguration(String key, AbstractEngineConfiguration engineConfiguration) {
		if (engineConfigurations == null) {
			engineConfigurations = new HashMap<>();
		}
		engineConfigurations.put(key, engineConfiguration);
	}

	public Map<String, AbstractServiceConfiguration> getServiceConfigurations() {
		return serviceConfigurations;
	}

	public AbstractEngineConfiguration setServiceConfigurations(Map<String, AbstractServiceConfiguration> serviceConfigurations) {
		this.serviceConfigurations = serviceConfigurations;
		return this;
	}

	public void addServiceConfiguration(String key, AbstractServiceConfiguration serviceConfiguration) {
		if (serviceConfigurations == null) {
			serviceConfigurations = new HashMap<>();
		}
		serviceConfigurations.put(key, serviceConfiguration);
	}

	public void setDefaultCommandInterceptors(Collection<? extends CommandInterceptor> defaultCommandInterceptors) {
		this.defaultCommandInterceptors = defaultCommandInterceptors;
	}

	public SqlSessionFactory getSqlSessionFactory() {
		return sqlSessionFactory;
	}

	public AbstractEngineConfiguration setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
		this.sqlSessionFactory = sqlSessionFactory;
		return this;
	}

	public boolean isDbHistoryUsed() {
		return isDbHistoryUsed;
	}

	public AbstractEngineConfiguration setDbHistoryUsed(boolean isDbHistoryUsed) {
		this.isDbHistoryUsed = isDbHistoryUsed;
		return this;
	}

	public DbSqlSessionFactory getDbSqlSessionFactory() {
		return dbSqlSessionFactory;
	}

	public AbstractEngineConfiguration setDbSqlSessionFactory(DbSqlSessionFactory dbSqlSessionFactory) {
		this.dbSqlSessionFactory = dbSqlSessionFactory;
		return this;
	}

	public TransactionFactory getTransactionFactory() {
		return transactionFactory;
	}

	public AbstractEngineConfiguration setTransactionFactory(TransactionFactory transactionFactory) {
		this.transactionFactory = transactionFactory;
		return this;
	}

	public TransactionContextFactory getTransactionContextFactory() {
		return transactionContextFactory;
	}

	public AbstractEngineConfiguration setTransactionContextFactory(TransactionContextFactory transactionContextFactory) {
		this.transactionContextFactory = transactionContextFactory;
		return this;
	}

	public int getMaxNrOfStatementsInBulkInsert() {
		return maxNrOfStatementsInBulkInsert;
	}

	public AbstractEngineConfiguration setMaxNrOfStatementsInBulkInsert(int maxNrOfStatementsInBulkInsert) {
		this.maxNrOfStatementsInBulkInsert = maxNrOfStatementsInBulkInsert;
		return this;
	}

	public boolean isBulkInsertEnabled() {
		return isBulkInsertEnabled;
	}

	public AbstractEngineConfiguration setBulkInsertEnabled(boolean isBulkInsertEnabled) {
		this.isBulkInsertEnabled = isBulkInsertEnabled;
		return this;
	}

	public Set<Class<?>> getCustomMybatisMappers() {
		return customMybatisMappers;
	}

	public AbstractEngineConfiguration setCustomMybatisMappers(Set<Class<?>> customMybatisMappers) {
		this.customMybatisMappers = customMybatisMappers;
		return this;
	}

	public Set<String> getCustomMybatisXMLMappers() {
		return customMybatisXMLMappers;
	}

	public AbstractEngineConfiguration setCustomMybatisXMLMappers(Set<String> customMybatisXMLMappers) {
		this.customMybatisXMLMappers = customMybatisXMLMappers;
		return this;
	}

	public Set<String> getDependentEngineMyBatisXmlMappers() {
		return dependentEngineMyBatisXmlMappers;
	}

	public AbstractEngineConfiguration setCustomMybatisInterceptors(List<Interceptor> customMybatisInterceptors) {
		this.customMybatisInterceptors = customMybatisInterceptors;
		return  this;
	}

	public List<Interceptor> getCustomMybatisInterceptors() {
		return customMybatisInterceptors;
	}

	public AbstractEngineConfiguration setDependentEngineMyBatisXmlMappers(Set<String> dependentEngineMyBatisXmlMappers) {
		this.dependentEngineMyBatisXmlMappers = dependentEngineMyBatisXmlMappers;
		return this;
	}

	public List<MybatisTypeAliasConfigurator> getDependentEngineMybatisTypeAliasConfigs() {
		return dependentEngineMybatisTypeAliasConfigs;
	}

	public AbstractEngineConfiguration setDependentEngineMybatisTypeAliasConfigs(List<MybatisTypeAliasConfigurator> dependentEngineMybatisTypeAliasConfigs) {
		this.dependentEngineMybatisTypeAliasConfigs = dependentEngineMybatisTypeAliasConfigs;
		return this;
	}

	public List<MybatisTypeHandlerConfigurator> getDependentEngineMybatisTypeHandlerConfigs() {
		return dependentEngineMybatisTypeHandlerConfigs;
	}

	public AbstractEngineConfiguration setDependentEngineMybatisTypeHandlerConfigs(List<MybatisTypeHandlerConfigurator> dependentEngineMybatisTypeHandlerConfigs) {
		this.dependentEngineMybatisTypeHandlerConfigs = dependentEngineMybatisTypeHandlerConfigs;
		return this;
	}

	public List<SessionFactory> getCustomSessionFactories() {
		return customSessionFactories;
	}

	public AbstractEngineConfiguration addCustomSessionFactory(SessionFactory sessionFactory) {
		if (customSessionFactories == null) {
			customSessionFactories = new ArrayList<>();
		}
		customSessionFactories.add(sessionFactory);
		return this;
	}

	public AbstractEngineConfiguration setCustomSessionFactories(List<SessionFactory> customSessionFactories) {
		this.customSessionFactories = customSessionFactories;
		return this;
	}

	public boolean isUsingRelationalDatabase() {
		return usingRelationalDatabase;
	}

	public AbstractEngineConfiguration setUsingRelationalDatabase(boolean usingRelationalDatabase) {
		this.usingRelationalDatabase = usingRelationalDatabase;
		return this;
	}

	public boolean isUsingSchemaMgmt() {
		return usingSchemaMgmt;
	}

	public AbstractEngineConfiguration setUsingSchemaMgmt(boolean usingSchema) {
		this.usingSchemaMgmt = usingSchema;
		return this;
	}

	public String getDatabaseTablePrefix() {
		return databaseTablePrefix;
	}

	public AbstractEngineConfiguration setDatabaseTablePrefix(String databaseTablePrefix) {
		this.databaseTablePrefix = databaseTablePrefix;
		return this;
	}

	public String getDatabaseWildcardEscapeCharacter() {
		return databaseWildcardEscapeCharacter;
	}

	public AbstractEngineConfiguration setDatabaseWildcardEscapeCharacter(String databaseWildcardEscapeCharacter) {
		this.databaseWildcardEscapeCharacter = databaseWildcardEscapeCharacter;
		return this;
	}

	public String getDatabaseCatalog() {
		return databaseCatalog;
	}

	public AbstractEngineConfiguration setDatabaseCatalog(String databaseCatalog) {
		this.databaseCatalog = databaseCatalog;
		return this;
	}

	public String getDatabaseSchema() {
		return databaseSchema;
	}

	public AbstractEngineConfiguration setDatabaseSchema(String databaseSchema) {
		this.databaseSchema = databaseSchema;
		return this;
	}

	public boolean isTablePrefixIsSchema() {
		return tablePrefixIsSchema;
	}

	public AbstractEngineConfiguration setTablePrefixIsSchema(boolean tablePrefixIsSchema) {
		this.tablePrefixIsSchema = tablePrefixIsSchema;
		return this;
	}

	public boolean isAlwaysLookupLatestDefinitionVersion() {
		return alwaysLookupLatestDefinitionVersion;
	}

	public AbstractEngineConfiguration setAlwaysLookupLatestDefinitionVersion(boolean alwaysLookupLatestDefinitionVersion) {
		this.alwaysLookupLatestDefinitionVersion = alwaysLookupLatestDefinitionVersion;
		return this;
	}

	public boolean isFallbackToDefaultTenant() {
		return fallbackToDefaultTenant;
	}

	public AbstractEngineConfiguration setFallbackToDefaultTenant(boolean fallbackToDefaultTenant) {
		this.fallbackToDefaultTenant = fallbackToDefaultTenant;
		return this;
	}

	/**
	 * @return name of the default tenant
	 * @deprecated use {@link AbstractEngineConfiguration#getDefaultTenantProvider()} instead
	 */
	@Deprecated
	public String getDefaultTenantValue() {
		return getDefaultTenantProvider().getDefaultTenant(null, null, null);
	}

	public AbstractEngineConfiguration setDefaultTenantValue(String defaultTenantValue) {
		this.defaultTenantProvider = (tenantId, scope, scopeKey) -> defaultTenantValue;
		return this;
	}

	public DefaultTenantProvider getDefaultTenantProvider() {
		return defaultTenantProvider;
	}

	public AbstractEngineConfiguration setDefaultTenantProvider(DefaultTenantProvider defaultTenantProvider) {
		this.defaultTenantProvider = defaultTenantProvider;
		return this;
	}

	public boolean isEnableLogSqlExecutionTime() {
		return enableLogSqlExecutionTime;
	}

	public void setEnableLogSqlExecutionTime(boolean enableLogSqlExecutionTime) {
		this.enableLogSqlExecutionTime = enableLogSqlExecutionTime;
	}

	public Map<Class<?>, SessionFactory> getSessionFactories() {
		return sessionFactories;
	}

	public AbstractEngineConfiguration setSessionFactories(Map<Class<?>, SessionFactory> sessionFactories) {
		this.sessionFactories = sessionFactories;
		return this;
	}

	public String getDatabaseSchemaUpdate() {
		return databaseSchemaUpdate;
	}

	public AbstractEngineConfiguration setDatabaseSchemaUpdate(String databaseSchemaUpdate) {
		this.databaseSchemaUpdate = databaseSchemaUpdate;
		return this;
	}

	public boolean isEnableEventDispatcher() {
		return enableEventDispatcher;
	}

	public AbstractEngineConfiguration setEnableEventDispatcher(boolean enableEventDispatcher) {
		this.enableEventDispatcher = enableEventDispatcher;
		return this;
	}

	public FlowableEventDispatcher getEventDispatcher() {
		return eventDispatcher;
	}

	public AbstractEngineConfiguration setEventDispatcher(FlowableEventDispatcher eventDispatcher) {
		this.eventDispatcher = eventDispatcher;
		return this;
	}

	public List<FlowableEventListener> getEventListeners() {
		return eventListeners;
	}

	public AbstractEngineConfiguration setEventListeners(List<FlowableEventListener> eventListeners) {
		this.eventListeners = eventListeners;
		return this;
	}

	public Map<String, List<FlowableEventListener>> getTypedEventListeners() {
		return typedEventListeners;
	}

	public AbstractEngineConfiguration setTypedEventListeners(Map<String, List<FlowableEventListener>> typedEventListeners) {
		this.typedEventListeners = typedEventListeners;
		return this;
	}

	public List<EventDispatchAction> getAdditionalEventDispatchActions() {
		return additionalEventDispatchActions;
	}

	public AbstractEngineConfiguration setAdditionalEventDispatchActions(List<EventDispatchAction> additionalEventDispatchActions) {
		this.additionalEventDispatchActions = additionalEventDispatchActions;
		return this;
	}

	public Clock getClock() {
		return clock;
	}

	public AbstractEngineConfiguration setClock(Clock clock) {
		this.clock = clock;
		return this;
	}

	public int getMaxLengthString() {
		if (maxLengthStringVariableType == -1) {
			if ("oracle".equalsIgnoreCase(databaseType)) {
				return DEFAULT_ORACLE_MAX_LENGTH_STRING;
			} else {
				return DEFAULT_GENERIC_MAX_LENGTH_STRING;
			}
		} else {
			return maxLengthStringVariableType;
		}
	}

	public int getMaxLengthStringVariableType() {
		return maxLengthStringVariableType;
	}

	public AbstractEngineConfiguration setMaxLengthStringVariableType(int maxLengthStringVariableType) {
		this.maxLengthStringVariableType = maxLengthStringVariableType;
		return this;
	}

	public List<EngineDeployer> getDeployers() {
		return deployers;
	}

	public AbstractEngineConfiguration setDeployers(List<EngineDeployer> deployers) {
		this.deployers = deployers;
		return this;
	}

	public List<EngineDeployer> getCustomPreDeployers() {
		return customPreDeployers;
	}

	public AbstractEngineConfiguration setCustomPreDeployers(List<EngineDeployer> customPreDeployers) {
		this.customPreDeployers = customPreDeployers;
		return this;
	}

	public List<EngineDeployer> getCustomPostDeployers() {
		return customPostDeployers;
	}

	public AbstractEngineConfiguration setCustomPostDeployers(List<EngineDeployer> customPostDeployers) {
		this.customPostDeployers = customPostDeployers;
		return this;
	}

	public boolean isEnableConfiguratorServiceLoader() {
		return enableConfiguratorServiceLoader;
	}

	public AbstractEngineConfiguration setEnableConfiguratorServiceLoader(boolean enableConfiguratorServiceLoader) {
		this.enableConfiguratorServiceLoader = enableConfiguratorServiceLoader;
		return this;
	}

	public List<EngineConfigurator> getConfigurators() {
		return configurators;
	}

	public AbstractEngineConfiguration addConfigurator(EngineConfigurator configurator) {
		if (configurators == null) {
			configurators = new ArrayList<>();
		}
		configurators.add(configurator);
		return this;
	}

	public AbstractEngineConfiguration setConfigurators(List<EngineConfigurator> configurators) {
		this.configurators = configurators;
		return this;
	}

	public EngineConfigurator getIdmEngineConfigurator() {
		return idmEngineConfigurator;
	}

	public AbstractEngineConfiguration setIdmEngineConfigurator(EngineConfigurator idmEngineConfigurator) {
		this.idmEngineConfigurator = idmEngineConfigurator;
		return this;
	}

	public AbstractEngineConfiguration setForceCloseMybatisConnectionPool(boolean forceCloseMybatisConnectionPool) {
		this.forceCloseMybatisConnectionPool = forceCloseMybatisConnectionPool;
		return this;
	}

	public boolean isForceCloseMybatisConnectionPool() {
		return forceCloseMybatisConnectionPool;
	}
}
